﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.UnitTesting
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Text;

    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    /// <summary>Provides methods that facilitate writing unit tests.</summary>
    /// <threadsafety static="true" instance="false"/>
    public abstract class TestBase
    {
        private readonly Random random;
        private int seed;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Executes each action in <paramref name="actions"/> and checks that each of them throws an exception
        /// of type <typeparamref name="TExcept"/>.</summary>
        /// <typeparam name="TException">The type of exception to check for.</typeparam>
        /// <param name="actions">The actions to execute.</param>
        /// <exception cref="UnexpectedSuccessException">An action in <paramref name="actions"/> did not throw the
        /// expected exception. Note: this exception is deliberately inaccessible to client code, so that it cannot be
        /// caught.</exception>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "There's no clean way around a type parameter here.")]
        public static void AssertThrow<TException>(params Action[] actions) where TException : Exception
        {
            if (actions == null)
            {
                throw new ArgumentNullException("actions");
            }

            foreach (Action action in actions)
            {
                if (action != null)
                {
                    // If the code in the following method is inlined here then the Visual Studio 2008 SP1 debugger
                    // acts as if TException is not caught and breaks execution. See
                    // http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=387231
                    AsserThrowImpl<TException>(action);
                }
            }
        }

        /// <summary>Tests all methods of a compiler generated enumerator (implemented with the C# yield keyword).
        /// </summary>
        /// <typeparam name="T">The type of the collection elements.</typeparam>
        /// <param name="enumerator">The enumerator to test.</param>
        /// <returns>The number of elements in the collection represented by <paramref name="enumerator"/>.</returns>
        public static int TestCompilerGeneratedEnumerator<T>(IEnumerator<T> enumerator)
        {
            using (enumerator)
            {
                return TestCompilerGeneratedEnumeratorImpl(enumerator, () => enumerator.Current);
            }
        }

        /// <summary>Tests all methods of a compiler generated enumerator (implemented with the C# yield keyword).
        /// </summary>
        /// <param name="enumerator">The enumerator to test.</param>
        /// <returns>The number of elements in the collection represented by <paramref name="enumerator"/>.</returns>
        public static int TestCompilerGeneratedEnumerator(IEnumerator enumerator)
        {
            return TestCompilerGeneratedEnumeratorImpl(enumerator, () => enumerator.Current);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Initializes a new instance of the <see cref="TestBase"/> class by calling
        /// <see cref="TestBase(int)"/>(<see cref="Environment.TickCount">Environment.TickCount</see>).</summary>
        /// <remarks>Deriving test classes should normally call this ctor.</remarks>
        protected TestBase() : this(Environment.TickCount)
        {
        }

        /// <summary>Initializes a new instance of the <see cref="TestBase"/> class by initializing the internal
        /// <see cref="Random"/> instance with <paramref name="seed"/>.</summary>
        /// <param name="seed">The seed to initialize the internal <see cref="Random"/> instance with.</param>
        /// <remarks>
        /// <para>Deriving test classes should only call this ctor to reproduce spurious test failures.</para>
        /// <para><paramref name="seed"/> is written to the console. In the case of a spurious failure, the seed value
        /// can be copied from the output of the failing test. The derived class constructor can then be changed to call
        /// this constructor with the seed value.</para>
        /// </remarks>
        protected TestBase(int seed)
        {
            this.random = new Random(seed);
            this.seed = seed;
        }

        /// <summary>Gets the internal <see cref="Random"/> instance that was initialized during construction.</summary>
        protected Random Random
        {
            get
            {
                if (this.seed != 0)
                {
                    // Console output is not captured during test class construction. That's why we only write it when
                    // the Random instance is fetched for the first time.
                    Console.WriteLine("Random seed: {0}", this.seed);
                    this.seed = 0;
                }

                return this.random;
            }
        }

        /// <summary>Returns a string of length 32 that is guaranteed to be unique.</summary>
        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Just like with Guid.NewGuid(), a property is not appropriate.")]
        protected static string GetUniqueString()
        {
            return Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
        }

        /// <summary>Gets a random length byte array filled with random ASCII values (1..127).</summary>
        /// <returns>The return value of <c><see cref="GetRandomAsciiByteArray(int)"/>(100)</c>.</returns>
        protected byte[] GetRandomAsciiByteArray()
        {
            return this.GetRandomAsciiByteArray(100);
        }

        /// <summary>Returns a random length byte array filled with random ASCII values (1..127).</summary>
        /// <param name="maxLength">The maximum length of the array.</param>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="maxLength"/> is smaller than 0 or equal to
        /// <see cref="int.MaxValue">int.MaxValue</see>.</exception>
        protected byte[] GetRandomAsciiByteArray(int maxLength)
        {
            if ((maxLength < 0) || (maxLength == int.MaxValue))
            {
                throw new ArgumentOutOfRangeException("maxLength");
            }

            byte[] result = new byte[Random.Next(0, (int)maxLength + 1)];

            for (int index = 0; index < result.Length; ++index)
            {
                // ASCII is only 0..127. \0 must not be included as that is the string terminator for legacy code.
                result[index] = (byte)Random.Next(1, 128);
            }

            return result;
        }

        /// <summary>Gets a random length string filled with random ASCII characters (1..127).</summary>
        /// <returns>The return value of <c><see cref="GetRandomString(int)"/>(100)</c>.</returns>
        protected string GetRandomString()
        {
            return this.GetRandomString(100);
        }

        /// <summary>Returns a random length string filled with random ASCII characters (1..127).</summary>
        /// <param name="maxLength">The maximum length of the string.</param>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="maxLength"/> is greater than
        /// <see cref="int.MaxValue">int.MaxValue</see>.</exception>
        protected string GetRandomString(int maxLength)
        {
            byte[] randomArray = this.GetRandomAsciiByteArray(maxLength);
            return Encoding.ASCII.GetString(randomArray, 0, randomArray.Length);
        }

        /// <summary>Tests standard exception constructors.</summary>
        /// <typeparam name="T">The type of the exception to test.</typeparam>
        /// <exception cref="ArgumentException">A constructor of <typeparamref name="T"/> does not propagate the
        /// innerException argument correctly.</exception>
        /// <exception cref="Exception">Some of the expected methods are not present. See message for more information.
        /// </exception>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "There's no clean way around a type parameter here.")]
        protected void TestStdExceptionConstructors<T>() where T : Exception, new()
        {
            T innerException = new T();
            CheckPropagation(innerException, null, null);

            string message = this.GetRandomString();
            CheckPropagation(Activator<T>.CreateInstance(message), message, null);
            CheckPropagation(Activator<T>.CreateInstance(message, innerException), message, innerException);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private static void AsserThrowImpl<TException>(Action action) where TException : Exception
        {
            try
            {
                action();
                throw new UnexpectedSuccessException();
            }
            catch (TException)
            {
            }
        }

        private static int TestCompilerGeneratedEnumeratorImpl<TResult>(
            IEnumerator enumerator, Func<TResult> getCurrentProperty)
        {
            int elementCount = 0;

            while (enumerator.MoveNext())
            {
                ++elementCount;
                TResult result = getCurrentProperty();
            }

            enumerator.MoveNext();
            AssertThrow<NotSupportedException>(() => enumerator.Reset());
            return elementCount;
        }

        [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "We want to indicate that there's something wrong with T.")]
        private static void CheckPropagation(
            Exception exception, string expectedMessage, Exception expectedInnerException)
        {
            if ((expectedMessage != null) && !object.ReferenceEquals(expectedMessage, exception.Message))
            {
                throw new ArgumentException(
                    "A constructor of this type does not propagate the message argument correctly.", "T");
            }

            if (expectedInnerException != exception.InnerException)
            {
                throw new ArgumentException(
                    "A constructor of this type does not propagate the innerException argument correctly.", "T");
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>The exception that is thrown when a test succeeds unexpectedly (as opposed to throwing an
        /// exception).</summary>
        [SuppressMessage("Microsoft.Design", "CA1064:ExceptionsShouldBePublic", Justification = "This one is intentionally private.")]
        [Serializable]
        private sealed class UnexpectedSuccessException : Exception
        {
            internal UnexpectedSuccessException()
            {
            }
        }
    }
}
